package de.axone.webtemplate;

import java.io.IOException;
import java.io.PrintWriter;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import de.axone.web.encoding.AmpEncoder;
import de.axone.web.encoding.AttributeEncoder;
import de.axone.web.encoding.Encoder;
import de.axone.web.encoding.HtmlEncoder;
import de.axone.web.encoding.UrlEncoder;
import de.axone.webtemplate.AbstractFileWebTemplate.ParserException;
import de.axone.webtemplate.form.Translator;
import de.axone.webtemplate.function.Function;

/**
 * 
 * @author flo
 * TODO: Doku. Ausserdem die sache mit dem HolderKey umbauen dass das die Attribute im DataHoderItem landen.
 */
public class DataHolder implements Cloneable {

	// List in proper order
	private LinkedList<DataHolderKey> keys;

	// Values
	private HashMap<String, DataHolderItem> data;

	// Parameters
	private HashMap<String, String> parameters;
	
	// Functions
	private HashMap<String, Function> functions;

	DataHolder() {
		this.keys = new LinkedList<DataHolderKey>();
		this.data = new HashMap<String, DataHolderItem>();
		this.parameters = new HashMap<String, String>();
		this.functions = new HashMap<String,Function>();
	}

	private DataHolder(LinkedList<DataHolderKey> keys,
			HashMap<String, DataHolderItem> data,
			HashMap<String, String> parameters) {

		this.keys = keys;
		this.data = data;
		this.parameters = parameters;
		this.functions = new HashMap<String,Function>();
	}

	List<DataHolderKey> getVariables() {

		return keys;
	}

	static int functionCount = 1;
	void addValue( String key, String value, DataHolderItemType type, boolean translate )
			throws KeyException, ParserException {
    	
		AttributeMap attributes = null;
		DataHolderEncodingType encoding = null;
		if( type == DataHolderItemType.VAR ){
			encoding = DataHolderEncodingType.matching( key );
			if( encoding != DataHolderEncodingType.defaultEncoding ){
				key = key.substring( 1, key.length()-1 );
			}
		
    		attributes = AttributeParser.parse( key );
		}
		
		DataHolderKey dhKey;
		
		if( attributes != null && attributes.size() > 1 ){
			key = (String)attributes.get( AttributeParser.TAG_NAME );
			dhKey = new DataHolderKey( key, attributes );
		} else {
			dhKey = new DataHolderKey( key, null );
		}
			
		keys.add( dhKey );

		if( !data.containsKey( key ) ) {
			
			data.put( dhKey.getName(), new DataHolderItem( key, value, type, encoding, null, translate ) );
		}
	}
	
	public void setValue( String key, Object value ) throws KeyException {

		if( data.containsKey( key ) ) {
			data.get( key ).setValue( value );
		}
	}
	
	public void setFunction( String key, Function function ) throws KeyException {
		functions.put( key, function );
	}

	public DataHolderItem getItem( String key ) throws KeyException {

		if( data.containsKey( key ) ) {
			return data.get( key );
		} else
			throw new KeyException( "Doesn't contain: " + key );
	}

	void putParameter( String key, String value ) {
		parameters.put( key.toLowerCase(), value );
	}

	public String getParameter( String key ) {
		return parameters.get( key.toLowerCase() );
	}
	
	public boolean has( String key ) {
		return data.containsKey( key );
	}

	public Set<String> getKeys() {
		return data.keySet();
	}
	
	public List<DataHolderKey> getFullKeys(){
		return keys;
	}

	public void clear() {
		for( String key : getKeys() ) {

			try {
				DataHolderItem item = getItem( key );

				if( item.getType() == DataHolderItemType.VAR )
					item.setValue( null );

			} catch( KeyException e ) {
				e.printStackTrace(); // Never happens
			}
		}
	}

	public DataHolder clone() {

		LinkedList<DataHolderKey> cloneKeys = new LinkedList<DataHolderKey>( keys );
		HashMap<String, DataHolderItem> cloneData = new HashMap<String, DataHolderItem>();
		for( String key : data.keySet() ) {
			cloneData.put( key, data.get( key ).clone() );
		}
		HashMap<String, String> cloneParameters = new HashMap<String, String>(
				parameters );

		DataHolder clone = new DataHolder( cloneKeys, cloneData,
				cloneParameters );

		return clone;
	}

	public void render( Object object, HttpServletRequest request,
			HttpServletResponse response,
			Translator translator ) throws IOException,
			WebTemplateException, Exception {

		PrintWriter out = response.getWriter();

		for( DataHolderKey key : keys ) {

			DataHolderItem item = data.get( key.getName() );
			Object value = item.getValue();
			
			Function function = null;
			String functionName;
			if( key.getAttributes() != null ){
				functionName = key.getAttributes().getAsStringRequired( AttributeParser.TAG_NAME );
			} else {
				functionName = key.getName();
			}
			if( functions.containsKey( functionName ) ){
				function = functions.get( functionName );
			}
			if( key.getAttributes() != null ){
				//E.rr( key.getAttributes() );
    			function = functions.get( functionName );
			}
			if( function != null ){
				if( key.getAttributes() != null ){
    				function.render( request, response, key.getAttributes(), value, translator );
				} else {
					function.render( request, response, new AttributeMap(), value, translator );
				}
			} else if( value != null ) {

				if( value instanceof String ) {

					String stringValue = (String) value;
					if( item.isTranslate() && translator != null ){
						
    					out.write( translator.translate( stringValue ) );
					} else {
    					out.write( stringValue );
					}

				} else if( value instanceof Renderer ) {

					Renderer renderer = (Renderer) value;
					renderer.render( object, request, response, translator );

				} else if( value instanceof Collection<?> ) {

					Collection<?> collection = (Collection<?>) value;

					for( Object o : collection ) {

						Renderer renderer = (Renderer) o;
						renderer.render( object, request, response, translator );
					}

				} else {
					
					String stringValue = value.toString();
					out.write( stringValue );
				}
			}
		}
	}

	@Override
	public String toString() {

		StringBuilder builder = new StringBuilder();

		builder.append( "CONTENT:\n" );
		for( DataHolderKey key : keys ) {

			DataHolderItem item = data.get( key.getName() );

			builder.append( item.toString() ).append( '\n' );
		}

		builder.append( "PARAMETERS:\n" );
		for( String key : parameters.keySet() ) {
			builder.append( key ).append( ": " ).append( parameters.get( key ) )
					.append( '\n' );
		}

		return builder.toString();
	}

	enum DataHolderItemType {

		TEXT, VAR;
	}
	
	static enum DataHolderEncodingType {
		
		none('(',')', null ), 
		attribute('#','#', new AttributeEncoder() ), 
		amp('[',']', new AmpEncoder() ), 
		url('@', '@', new UrlEncoder() ),
		html('{','}', new HtmlEncoder() ),
		defaultEncoding( null, null, null );
		
		Character begin, end;
		Encoder encoder;
		
		DataHolderEncodingType( Character begin, Character end, Encoder encoder ){
			this.begin=begin;
			this.end=end;
			this.encoder = encoder;
		}
		static DataHolderEncodingType matching( String probe ){
			
			if( probe == null || probe.length() < 2 ) return defaultEncoding;
			
			for( DataHolderEncodingType encoding : DataHolderEncodingType.values() ){
    					
				if( 
					encoding.begin != null 
					&& probe.charAt( 0 ) == (char)encoding.begin
					&& probe.charAt( probe.length()-1 ) == (char)encoding.end
				){
					return encoding;
				}
			}
			return defaultEncoding;
		}
	};
		
	static class DataHolderItem implements Cloneable {

		private String name;
		private Object value;
		private DataHolderItemType type;
		private boolean translate;
		private DataHolderEncodingType encoding;
		private HashMap<String,Object> attributes;

		public DataHolderItem(String name, Object value,
				DataHolderItemType type, DataHolderEncodingType encoding, HashMap<String,Object> attributes , boolean translate) {

			setEncoding( encoding );
			setName( name );
			setValue( value );
			setType( type );
			setAttributes( attributes );
			setTranslate( translate );
		}

		public String getName() {
			return name;
		}

		public void setName( String name ) {
			this.name = name;
		}

		public Object getValue() {
			
			if( value != null && value instanceof String && encoding != null && encoding.encoder != null ){
    			return encoding.encoder.encode( (String)value );
    		} else {
    			return value;
    		}
		}

		public void setValue( Object value ) {
			this.value = value;
		}

		public DataHolderItemType getType() {
			return type;
		}

		public void setType( DataHolderItemType type ) {
			this.type = type;
		}

		public DataHolderEncodingType getEncoding() {
			return encoding;
		}

		public void setEncoding( DataHolderEncodingType encoding ) {
			this.encoding = encoding;
		}

		public HashMap<String, Object> getAttributes() {
			return attributes;
		}

		public void setAttributes( HashMap<String, Object> attributes ) {
			this.attributes = attributes;
		}

		public boolean isTranslate() {
			return translate;
		}

		public void setTranslate( boolean translate ) {
			this.translate = translate;
		}

		@Override
		public String toString() {
			return name + ": " + value + " (" + type + ")";
		}

		@Override
		public DataHolderItem clone() {

			return new DataHolderItem( name, value, type, encoding, attributes, translate );
		}
	}
	
	public static class DataHolderKey {
		String name;
		AttributeMap attributes;
		
		DataHolderKey( String name, AttributeMap attributes ){
			this.name = name;
			this.attributes = attributes;
		}
		
		@Override
		public int hashCode(){
			
			int hashCode = name.hashCode();
			
			if( attributes != null ){
				hashCode ^= attributes.hashCode();
			}
			
			return hashCode;
		}

		public String getName() {
			return name;
		}

		public AttributeMap getAttributes() {
			return attributes;
		}
		
	}

}